package io.xxx.xbutler.core.callback;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import io.xxx.xbutler.common.RedisKeys;
import io.xxx.xbutler.data.PushRecordItemMapper;
import io.xxx.xbutler.data.PushRecordMapper;
import io.xxx.xbutler.data.TaskMapper;
import io.xxx.xbutler.domain.Callback;
import io.xxx.xbutler.domain.task.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.CheckReturnValue;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 推送结果处理器（同时支持私发和群发）
 */
@Slf4j
@Component
public class PushResultHandler {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    protected TransactionTemplate transactionTemplate;

    @Resource
    protected PushRecordMapper pushRecordMapper;

    @Resource
    protected PushRecordItemMapper pushRecordItemMapper;

    @Resource
    private TaskMapper taskMapper;

    public void handle(Callback callback, Long taskId, ReceiverType receiverType, String receiverId) {
        LocalDateTime now = LocalDateTime.now();
        String serialNo = callback.getSerialNo();

        PushRecord pushRecord = getPushRecord(taskId, receiverType, receiverId);
        List<PushRecordItem> items = getPushRecordItems(pushRecord.getId());
        if (ObjectUtils.isEmpty(items)) {
            log.error("任务推送记录明细为空[taskId:{}, receiverType:{}, receiverId:{}]", taskId, receiverType, receiverId);
            return;
        }

        JSONObject data = (JSONObject) callback.getData();
        List<PushRecordItem> recordItems;
        if (!callback.isSuccess()) {
            recordItems = items.stream()
                    .map(it -> {
                        PushRecordItem item = new PushRecordItem()
                                .setId(it.getId())
                                .setStatus(SendStatus.FAIL)
                                .setUpdatedTime(now);
                        LocalDateTime pushEndTime = callback.getTimestamp();
                        item.setPushEndTime(pushEndTime)
                                .setPushElapsedTime(Duration.between(it.getPushStartTime(), pushEndTime).abs().toMillis());
                        return item;
                    })
                    .toList();
        } else {
            Map<Integer, JSONObject> msgData = data.getJSONArray("MsgData")
                    .stream()
                    .map(JSONObject::from)
                    .collect(Collectors.toMap(it -> it.getInteger("nMsgNum"), it -> it));
            recordItems = items.stream()
                    .map(it -> {
                        PushRecordItem item = new PushRecordItem()
                                .setId(it.getId())
                                .setStatus(SendStatus.SUCCESS)
                                .setUpdatedTime(now);
                        JSONObject msgDatum = msgData.get(it.getSn());
                        Instant msgTime = msgDatum.getInstant("dtMsgTime");
                        if (msgTime != null) {
                            LocalDateTime pushEndTime = LocalDateTime.ofInstant(msgTime, ZoneId.systemDefault());
                            item.setPushEndTime(pushEndTime)
                                    .setPushElapsedTime(Duration.between(it.getPushStartTime(), pushEndTime).abs().toMillis());
                        }
                        return item;
                    })
                    .toList();
        }

        Task task = taskMapper.selectById(taskId);
        Long pushRecordId = items.getFirst().getRecordId();
        RSet<Long> pushSuccessPushRecords = redissonClient.getSet(RedisKeys.pushSuccessPushRecords(taskId));
        RSet<Long> pushFailPushRecords = redissonClient.getSet(RedisKeys.pushFailPushRecords(taskId));
        Duration expire = Duration.between(task.getStartTime(), task.getEndTime())
                .abs()
                .plusDays(1);
        pushSuccessPushRecords.expireIfNotSet(expire);
        pushFailPushRecords.expireIfNotSet(expire);

        JSONArray errorMsgData = data.getJSONArray("errorMsgData"); // 如果包含失败消息，此次推送标记为失败
        if (!ObjectUtils.isEmpty(errorMsgData)) {
            pushFailPushRecords.add(pushRecordId);
            Map<Integer, JSONObject> errorMsgDataMap = errorMsgData
                    .stream()
                    .map(JSONObject::from)
                    .collect(Collectors.toMap(it -> it.getInteger("nMsgNum"), it -> it));
            ErrorSource errorSource = ErrorSource.TAN_JING;
            for (PushRecordItem item : items) {
                JSONObject errorMsgDatum = errorMsgDataMap.get(item.getSn());
                Integer errorCode = errorMsgDatum.getInteger("Code");
                item.setErrorSource(errorSource)
                        .setErrorCode(errorCode)
                        .setErrorMessage("【" + errorSource.getName() + "】" + errorCode + ": " + errorMsgDatum.getString("Message"))
                        .setStatus(SendStatus.FAIL);
            }
        } else {
            pushSuccessPushRecords.add(pushRecordId);
        }

        String messageId = data.getString("vcMsgId");
        int pushSuccessCount = pushSuccessPushRecords.size();
        int pushFailCount = pushFailPushRecords.size();
        double process = 100.0 * (pushSuccessCount + pushFailCount) / task.getPushCount();
        transactionTemplate.executeWithoutResult(_ -> {
            taskMapper.update(getTaskWrapper(taskId, pushSuccessCount, pushFailCount, process));
            pushRecordMapper.update(getPushRecordWrapper(pushRecordId, serialNo, messageId, now));
            pushRecordItemMapper.updateById(recordItems);
        });
    }

    private LambdaUpdateWrapper<Task> getTaskWrapper(Long taskId,
                                                     Integer pushSuccessCount,
                                                     Integer pushFailCount,
                                                     double process) {
        LambdaUpdateWrapper<Task> wrapper = new LambdaUpdateWrapper<Task>()
                .set(Task::getPushSuccessCount, pushSuccessCount)
                .set(Task::getPushFailCount, pushFailCount)
                .set(Task::getProcess, process)
                .eq(Task::getId, taskId);
        if (process == 100) {
            wrapper.set(Task::getStatus, TaskStatus.COMPLETED);
        }
        return wrapper;
    }

    private LambdaUpdateWrapper<PushRecord> getPushRecordWrapper(Long pushRecordId,
                                                                 String serialNo,
                                                                 String messageId,
                                                                 LocalDateTime now) {
        return new LambdaUpdateWrapper<PushRecord>()
                .set(PushRecord::getSerialNo, serialNo)
                .set(PushRecord::getMessageId, StringUtils.hasText(messageId) ? messageId : null)
                .set(PushRecord::getUpdatedTime, now)
                .eq(PushRecord::getId, pushRecordId);
    }

    @CheckReturnValue
    protected PushRecord getPushRecord(Long taskId, ReceiverType receiverType, String receiverId) {
        LambdaQueryWrapper<PushRecord> wrapper = new LambdaQueryWrapper<PushRecord>()
                .eq(PushRecord::getTaskId, taskId)
                .eq(PushRecord::getReceiverType, receiverType)
                .eq(PushRecord::getReceiverId, receiverId);
        return pushRecordMapper.selectOne(wrapper);
    }

    @CheckReturnValue
    protected List<PushRecordItem> getPushRecordItems(Long recordId) {
        LambdaQueryWrapper<PushRecordItem> wrapper = new LambdaQueryWrapper<PushRecordItem>()
                .eq(PushRecordItem::getRecordId, recordId);
        return pushRecordItemMapper.selectList(wrapper);
    }
}
